require "Tokenizer"
require "Messages"
require "SyntaxObjs"
require "Logger"
require "pat"

class Parser
  def initialize(tokenizer, log=nil)
      @tok = tokenizer
      @log = log ? log : Logger.new(STDOUT)
      @sym_table  = Pat.new(:name => "Application")
      @tables     = Array.new.push(@sym_table)
      set_keywords_parsers
  end
  
  def to_s
    "[Parser] operating #{@tok}"
  end
  
  def parse
     @log.debug("[Parser] parse #{@tok.get_token.info()}")
    @tok.next_token
    stmts = Array.new()

    while !@tok.get_token.kind_of?(EndFileToken)
      stmts.push(parse_statement)
    end
    object_generator SynProgram, stmts, sym_table
  end
  
  def print_sym_tables()
    Pat.all_pats.each {|p| puts p,"\n"}
  end

private
  def parse_expr_assign()
    @log.debug("[Parser] parse_expr_assign #{@tok.get_token.info()}")
    
    left = parse_logic_exp_or
    if @tok.get_token.value == '='
      @tok.next_token
      object_generator BinaryOp, '=', left, parse_expr_assign
    else
      left
    end
  end
  
  def parse_logic_exp_or(left=nil)
    @log.debug("[Parser] parse_logic_expression_or #{@tok.get_token.info()}")
    
    left ||= parse_logic_exp_and
    if @tok.get_token.value == '||'
      @tok.next_token
      right = parse_logic_exp_and
      parse_logic_exp_or object_generator(BinaryOp, '||', left, right)
    else
      left
    end
  end
  
  def parse_logic_exp_and(left=nil)
    @log.debug("[Parser] parse_logic_expression_and #{@tok.get_token.info()}")
    
    left ||= parse_logic_exp_eq
    if @tok.get_token.value == '&&'
      @tok.next_token
      right = parse_logic_exp_eq
      parse_logic_exp_and object_generator(BinaryOp, '&&', left, right)
    else
      left
    end
  end
  
  def parse_logic_exp_eq
    @log.debug("[Parser] parse_logic_expression_eq #{@tok.get_token.info()}")
    
    left = parse_logic_exp_comp
    case @tok.get_token.value
    when '=='
      @tok.next_token
      object_generator BinaryOp, '==', left, parse_logic_exp_comp
    when '!='
      @tok.next_token
      object_generator BinaryOp, '!=', left, parse_logic_exp_comp
    else
      left
    end
  end
  
  def parse_logic_exp_comp
    @log.debug("[Parser] parse_logic_expression_comp #{@tok.get_token.info()}")
    
    left = parse_expr
    t = @tok.get_token.value
    if ['<', '>', '<=', '>='].include?(t)
      @tok.next_token
      object_generator BinaryOp, t, left, parse_expr
    else
      left
    end
  end
  
  def parse_expr(left=nil)
    @log.debug("[Parser] parse_expr #{@tok.get_token.info()}")
    
    left ||= parse_term
    case @tok.get_token.value
    when '+'
      @tok.next_token
      object_generator BinaryOp, '+', left, parse_expr
    when '-'
      @tok.next_token
      right = parse_term
      parse_expr object_generator(BinaryOp, '-', left, right)
    else
      left
    end
  end
  
  def parse_term
    @log.debug("[Parser] parse_term #{@tok.get_token.info()}")
    
    left = parse_factor
    case @tok.get_token.value
    when '*'
      @tok.next_token
      object_generator BinaryOp, '*', left, parse_term
    when '/'
      @tok.next_token
      object_generator BinaryOp, '/', left, parse_term
    else
      return left
    end
  end
  
  def parse_factor
    @log.debug("[Parser] parse_factor #{@tok.get_token.info()}")
    
    t = @tok.get_token
    if t.kind_of?(OperationToken) && t.value == '-'
      #unary minus
      @tok.next_token
      object_generator UnaryOp, '-', parse_factor
    elsif t.kind_of?(OperationToken) && t.value == '!'
      @tok.next_token
      object_generator UnaryOp, '!', parse_factor
    elsif t.kind_of?(IdentifierToken)
      parse_name_seq
    elsif t.kind_of?(IntegerToken)
      @tok.next_token
      SynConst.new(t.value)
    elsif t.kind_of?(FloatToken)
      @tok.next_token
      object_generator SynConst, t.value
    elsif t.kind_of?(StringToken)
      @tok.next_token
      object_generator SynStrConst, t.value
    elsif t.value == '('
      @tok.next_token
      e = parse_expr_assign
      require_or_die ')',  p_message(:no_r_bracket)
      e
    elsif t.value == '['
      @tok.next_token
      arr = parse_array_def
      object_generator SynArrayConst, arr
    end
  end
  
  def parse_array_def
    @log.debug("[Parser] ParseArrayDef #{@tok.get_token.info()}")
    
    list = parse_list_declaration ',', ']', :no_r_bracket
  end

  #parse block of statements, eat 'end' keywoard and return SynBlock
  def parse_block
    @log.debug("[Parser] ParseBlock #{@tok.get_token.info()}")
    
    stmts = Array.new()
    while ((t = @tok.get_token).value != "end") && !t.kind_of?(EndFileToken)
      stmts.push(parse_statement)
    end
    
    if @tok.get_token.kind_of?(EndFileToken)
      raise CompilerException.new(@tok.line, @tok.pos), p_message(:no_end)
    end
    @tok.next_token #eat end
    object_generator SynBlock, stmts
  end
  
  def parse_statement
    @log.debug("[Parser] parse_statement")
    token = @tok.get_token
    
    if token.kind_of?(EndFileToken)
      return false
    elsif token.kind_of?(KeywordToken)
      if @keyword_parser[token.value].nil?
        raise CompilerException.new(@tok.line, @tok.pos), "No handler for keyword #{token}"
      else
        @keyword_parser[token.value].call
      end
    elsif expr = parse_expr_assign
      require_or_die ';',  p_message(:no_delimeter)
      expr
    else
      false
    end
  end

  def set_keywords_parsers
    @keyword_parser = {
      "if" => lambda {
        @tok.next_token #eat if
        new_sym_table
        mess = "%s after 'if' at <%d,%d>"
        # require_or_die '(', sprintf(mess, p_message(:no_open_par),@tok.line, @tok.pos)
        expr = parse_expr_assign
        # require_or_die ')', sprintf(mess, p_message(:no_close_par), @tok.line, @tok.pos)
        require_or_die ':', sprintf(mess, p_message(:general), @tok.line, @tok.pos)
        stmts = Array.new()
        while (((t = @tok.get_token).value != "end") && (t.value != 'else')) && !t.kind_of?(EndFileToken)
          stmts.push(parse_statement)
        end
        bl = object_generator SynBlock, stmts
        t = @tok.get_token.value
        if (t == 'end')
          @tok.next_token #eat end
          if_obj = object_generator SynIf, sym_table, expr, bl
          restore_sym_table
          return if_obj
        elsif (t == 'else')
          @tok.next_token #eat else
          block = parse_block
          if_obj = object_generator SynIf, sym_table, expr, bl, block
          restore_sym_table
          return if_obj
        end
      },
      "while" => lambda {
        @tok.next_token #eat while
        new_sym_table
        mess = "%s after 'while' at <%d,%d>"
        # require_or_die '(', sprintf(mess, p_message(:no_open_par),@tok.line, @tok.pos)
        expr = parse_expr_assign
        # require_or_die ')', sprintf(mess, p_message(:no_close_par), @tok.line, @tok.pos)
        require_or_die ':', sprintf(mess, p_message(:general), @tok.line, @tok.pos)
        block = parse_block
        while_obj = object_generator SynWhile, sym_table(), expr, block
        restore_sym_table
        return while_obj
      },
      "for" => lambda {
        @tok.next_token #eat for
        new_sym_table
        mess = "%s after 'for' at <%d,%d>"
        # require_or_die '(', sprintf(mess, p_message(:no_open_par),@tok.line, @tok.pos)
        expr = Array.new()
        3.times { |i|
          expr.push(parse_expr_assign)
          i == 2 || require_or_die(';', p_message(:no_delimeter))
        }
        # require_or_die ')', sprintf(mess, p_message(:no_close_par), @tok.line, @tok.pos)
        require_or_die ':', sprintf(mess, p_message(:general), @tok.line, @tok.pos)
        for_obj = object_generator SynFor, sym_table, expr[0], expr[1], expr[2], parse_block
        restore_sym_table
        return for_obj
      },
      "def" => lambda {
        @tok.next_token #eat def
        mess = "%s after 'def' at <%d,%d>"
        t = @tok.get_token
        name = parse_name_seq(false, true) || false
        
        new_sym_table name
        require_or_die '(', sprintf(mess, p_message(:no_open_par),@tok.line, @tok.pos)
        
        args = Array.new()
        while t = parse_defination_name
          args.push(t)
          if @tok.get_token.value == ','
            @tok.next_token
          else
            break
          end
        end
        require_or_die ')', sprintf("%s after 'def' arguments list", p_message(:no_close_par),@tok.line, @tok.pos)
        require_or_die ':', sprintf(mess, p_message(:general), @tok.line, @tok.pos)
        body = parse_block
        def_sym_table = sym_table
        restore_sym_table
        obj = if name
          f_def = object_generator FuncDefination, def_sym_table, args, body
          object_generator BinaryOp, '=', name, f_def
        else
          object_generator FuncDefination, def_sym_table, args, body
        end
        return obj
      },
      "return" => lambda {
        @tok.next_token #eat lambda
        mess = "%s after 'return' at <%d,%d>"
        var = parse_expr_assign
        require_or_die ';', sprintf(mess, p_message(:no_delimeter), @tok.line, @tok.pos)
        object_generator SynReturn, var
      },
    }
  end
  
  #parse input and return syntax object which represent variable or object attribute
  def parse_name_seq(base=false, is_def_name=false)
      name = parse_full_name(base, is_def_name)
      if @tok.get_token.value == '.'
        new_sym_table name
        @tok.next_token
        suff  = parse_name is_def_name
        obj   = object_generator SynObjectSelection, name, suff
        ret   = parse_name_seq(obj, is_def_name)
        restore_sym_table
        return ret
      end
      return name
  end
  
  def parse_full_name(left=false, is_def_name=false)
    base_name = left || parse_name(is_def_name)
    if array_index = parse_array_index
      object_generator SynArraySelection, base_name, array_index
    else
      base_name
    end
  end
  
  def parse_name(is_def_name=false)
    t = @tok.get_token
    t.kind_of?(IdentifierToken) || return
    name = t.value
    t = @tok.next_token
    if (t.value == '(') && !is_def_name
      #function call
      @tok.next_token
      args = parse_list_declaration ',', ')', :no_close_bracket
      object_generator SynFunctionCall, name, args
    else
      object_generator SynVar, name
    end
  end
  
  def parse_defination_name
    t = @tok.get_token
    t.kind_of?(KeywordToken) || return
    
    unless ['int', 'float', 'string', 'array'].include?(t.value)
      raise CompilerException.new(@tok.line, @tok.pos), p_message(:no_type_spec)
    end
    
    v = @tok.get_token
    t = @tok.next_token
    n = parse_name(true)
    sym_table[n].syn_type = v.value.to_sym
    sym_table[n].object.syn_type = v.value.to_sym if !sym_table[n].object.nil?
    return n
  end
  
  def parse_array_index
    t = @tok.get_token
    (t.value == '[') || return
    @tok.next_token
    e = parse_expr
    require_or_die ']', p_message(:no_r_bracket)
    return e
  end
  
  def require_or_die(sym, message)
    unless @tok.require_token(sym)
      raise CompilerException.new(@tok.line, @tok.pos), message
    end  
  end
  
  def p_message(message)
    return Messages[:parser][message]
  end
  
  def iterate()
    while (!(t = @tok.get_token).kind_of?(EndProgramToken) && !t.kind_of?(EndFileToken) && yield(t))
    end
  end
  
  def new_sym_table(name="")
    if name.respond_to?(:sym_table)
      @sym_table = name.sym_table
    else
      @sym_table = Pat.new(:parent => @sym_table, :name => name)
    end
    @tables.push(@sym_table)
  end
  
  def sym_table
    @sym_table
  end
  
  def restore_sym_table
    @tables.pop
    @sym_table = @tables.last
  end
  
  def object_generator(obj_class, *args)
    obj = obj_class.new(*args)
    
    case
    when (obj_class == BinaryOp) && (obj.op == '=')
      if obj.right.kind_of?(SynArrayConst) && obj.left.kind_of?(SynVar)
        obj.left = obj.left.to_syn_array
        sym_table[obj.left] = SymTableRecord.new(:array, obj.left)
        set_symbol_type obj
      elsif obj.left.respond_to?(:is_left_value?) && obj.left.is_left_value?
        set_symbol_type obj
      else
        raise CompilerException.new(@tok.line, @tok.pos), p_message(:non_left_value)
      end
      
    when obj_class == SynFunctionCall && SynParrotCall.op_codes.include?(args[0])
      obj = SynParrotCall.new(*args)
      
    when (obj_class == SynArraySelection)
      if sym_table[obj.variable].object.nil?
        arr = obj.variable.to_syn_array()
        sym_table[obj.variable] = SymTableRecord.new(:array, arr)
      elsif obj.index.kind_of?(Integer)
        a = sym_table[obj.syn_name].object.sym_table[obj.index]
        sym_table[obj.syn_name].object.sym_table[obj.index] = SymTableRecord.new(:undef, nil) if a.nil?
        obj.variable = sym_table[obj.syn_name].object
      else
        obj.variable = sym_table[obj.syn_name].object
      end
    when (obj_class == SynVar)
      from_s_table = sym_table[obj.name]
      if from_s_table.nil?
        sym_table[obj.name] = SymTableRecord.new(:undef, nil)
      elsif from_s_table.syn_type == :object
        obj = from_s_table.object
      end
      
    when (obj_class == SynObjectSelection)
      base_object, cell = args
      s_table = sym_table
      
      if !cell.nil? && cell.kind_of?(SynFunctionCall) && base_object.kind_of?(SynObjectVar) && !base_object.has_cell?(cell)
        raise CompilerException.new(@tok.line, @tok.pos), p_message(:unknown_method)
      end
      
      if base_object.kind_of?(SynVar)
        base_object = base_object.to_syn_object(sym_table).set_cell(cell)
        #TODO look later. maybe error with disapeared variable
        s_table.parent[base_object] = SymTableRecord.new(:object, base_object)
      elsif base_object.kind_of?(SynObjectVar)
        base_object.set_cell(cell)
      elsif base_object.kind_of?(SynObjectSelection)
        base_object.right = base_object.right.to_syn_object(sym_table).set_cell(cell)
        sym_table.parent[base_object.right] = SymTableRecord.new(:object, base_object.right)
        
        sym_table[cell] = SymTableRecord.new(:undef, cell)
      elsif base_object.kind_of?(SynArraySelection)
        base_object.set_cell(cell)
      else
        base_object = base_object.to_syn_object(sym_table).set_cell(cell)
        s_table[base_object] = SymTableRecord.new(:object, base_object)
      end
      obj = SynObjectSelection.new(base_object, cell)
    end
    
    return obj
  end
  
  def get_obj_type(syn_obj)
    if syn_obj.kind_of?(SynVar)
      if sym_table[syn_obj].nil?
        :undef
      else
        sym_table[syn_obj].syn_type
      end
    elsif syn_obj.kind_of?(UnaryOp)
      get_obj_type syn_obj.arg
    else
      syn_obj.syn_type
    end
  end
  
  def parse_list_declaration(delimeter, end_symbol, error)
    elems = Array.new
    while (t = @tok.get_token) && !t.kind_of?(EndFileToken)
      t.value == end_symbol && break
      elems.push(parse_expr_assign)
      @tok.get_token.value == end_symbol && break
      require_or_die delimeter, p_message(error)
    end
    @tok.next_token
    return elems
  end
  
  def get_syn_table_for_obj(base_obj)
    if base_obj.kind_of?(SynObjectSelection)
      base_obj.sym_table
    else
      sym_table
    end
  end
  
  def set_symbol_type(obj)
    if obj.left.kind_of?(SynObjectSelection)
      selection = obj.left
      selection.object.sym_table[selection.right].syn_type = get_obj_type obj.right
    elsif obj.left.kind_of?(SynArraySelection) && !obj.left.index.kind_of?(BinaryOp)
      s_table = sym_table[obj.left.variable].object.sym_table
      ot = get_obj_type obj.right
      s_table[obj.left.index] = SymTableRecord.new(ot, obj.right) if s_table[obj.left.index].nil?
    elsif obj.right.kind_of?(SynArrayConst)
      s_table = sym_table[obj.left].object.sym_table
      arr = obj.right.value
      arr.each_index do |i|
        s_table[i] = SymTableRecord.new(get_obj_type(arr[i]), arr[i]) if s_table[i].nil?
      end
    else
      if sym_table[obj.left].nil? || sym_table[obj.left].syn_type == :undef
        sym_table[obj.left] = SymTableRecord.new(get_obj_type(obj.right), obj.right)
      end
    end
  end
  
  def hide_sym_table    
    @sym_table = {}
  end
  
  def unhide_sym_table
    @sym_table = @tables.first
  end
  
  def inference_type(*a)
    #types are :string > :float > :int
    get_obj_type a.max {|a,b| get_obj_type(a).to_s.length <=> get_obj_type(b).to_s.length}
  end
end
